﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace BitLaboratory.DbAccess
{
    public class DataAccess : IDataAccess
    {
        private IDataAccessFactory _dataAccessBuilder;
        private string _commandText;
        private CommandType _commandType = System.Data.CommandType.StoredProcedure;
        private List<IDataParameter> _parameters = new List<IDataParameter>();

        public DataAccess(IDataAccessFactory dataAccessBuilder)
        {
            this._dataAccessBuilder = dataAccessBuilder;
        }

        public DataAccess SetCommandText(String commandText)
        {
            _commandText = commandText;
            return this;
        }

        public DataAccess SetCommandType(CommandType commandType)
        {
            _commandType = commandType;
            return this;
        }

        public DataAccess AddParameter(String parameterName, object parameterValue)
        {
            IDbDataParameter parameter = _dataAccessBuilder.DatabaseProvider.CreateDbParameter(parameterName, parameterValue);
            this._parameters.Add(parameter);
            return this;
        }

        public DataAccess AddOutputParameter(String parameterName, object parameterValue)
        {
            IDbDataParameter parameter = CreateDbParameter(parameterName, parameterValue, ParameterDirection.Output);
            this._parameters.Add(parameter);
            return this;
        }

        public DataAccess AddInputOutputParameter(String parameterName, object parameterValue)
        {
            IDbDataParameter parameter = CreateDbParameter(parameterName, parameterValue, ParameterDirection.InputOutput);
            this._parameters.Add(parameter);
            return this;
        }

        public DataAccess AddReturnValueParameter()
        {
            IDbDataParameter parameter = CreateDbParameter("RETURN_VALUE", null, ParameterDirection.ReturnValue);
            this._parameters.Add(parameter);
            return this;
        }

        private IDbDataParameter CreateDbParameter(String parameterName, object parameterValue, ParameterDirection direction)
        {
            IDbDataParameter parameter = _dataAccessBuilder.DatabaseProvider.CreateDbParameter(parameterName, parameterValue, direction);
            return parameter;
        }

        private IDbCommand CreateCommand(IDbConnection connection)
        {
            IDbCommand command = connection.CreateCommand();
            command.CommandType = _commandType;
            command.CommandText = _commandText;
            foreach (IDataParameter dataParameter in _parameters)
            {
                command.Parameters.Add(dataParameter);
            }
            return command;
        }

        private TEntity FillObject<TEntity>(DataTable data)
            where TEntity : new()
        {
            if (data.Rows.Count == 0) return default(TEntity);
            Type typeFromHandle = typeof(TEntity);
            TEntity result;
            if (data != null && data.Rows.Count > 0)
            {
                result = FillObject<TEntity>(data.Rows[0]);
            }
            else
            {
                result = default(TEntity);
            }
            return result;
        }

        private TEntity FillObject<TEntity>(DataRow dataRow)
            where TEntity : new()
        {
            if (dataRow == null) return default(TEntity);
            Type typeFromHandle = typeof(TEntity);
            PropertyInfo[] properties = typeFromHandle.GetProperties();
            TEntity result = (!typeFromHandle.IsValueType ? Activator.CreateInstance<TEntity>() : default(TEntity));
            for (int i = 0; i < properties.Length; i++)
            {
                PropertyInfo propertyInfo = properties[i];
                if (dataRow.Table.Columns.Contains(propertyInfo.Name))
                {
                    object obj = dataRow[propertyInfo.Name];
                    if (obj != DBNull.Value)
                    {
                        try
                        {
                            propertyInfo.SetValue(result, obj, null);
                        }
                        catch (Exception ex)
                        {
                            throw new Exception(String.Format("Property name: {0}, Exception: {1}", propertyInfo.Name, ex.Message), ex);
                        }
                    }
                }
            }
            return result;
        }

        private T Convert<T>(object input)
        {
            if (input == DBNull.Value)
                return default(T);
            return (T)input;
        }

        public T ExecuteScalar<T>()
        {
            using (IDbConnection connection = _dataAccessBuilder.DatabaseProvider.CreateDbConnection())
            {
                IDbCommand command = CreateCommand(connection);
                connection.Open();
                object ret = command.ExecuteScalar();
                return Convert<T>(ret);
            }
        }

        public DataTable ExecuteFillDataTable()
        {
            using (IDbConnection connection = _dataAccessBuilder.DatabaseProvider.CreateDbConnection())
            {
                IDbCommand command = CreateCommand(connection);
                IDbDataAdapter dataAdapter = _dataAccessBuilder.DatabaseProvider.CreateDbDataAdapter(command);
                DataSet data = new DataSet();
                dataAdapter.Fill(data);
                return data.Tables[0];
            }
        }

        public int ExecuteNonQuery()
        {
            using (IDbConnection connection = _dataAccessBuilder.DatabaseProvider.CreateDbConnection())
            {
                IDbCommand command = CreateCommand(connection);
                connection.Open();
                return command.ExecuteNonQuery();
            }
        }

        public TEntity ExecuteFillObject<TEntity>()
            where TEntity : class, new()
        {
            return FillObject<TEntity>(ExecuteFillDataTable());
        }

        public List<TEntity> ExecuteFillObjects<TEntity>()
            where TEntity : class, new()
        {
            List<TEntity> list = new List<TEntity>();
            DataTable data = ExecuteFillDataTable();
            foreach (DataRow row in data.Rows)
            {
                list.Add(FillObject<TEntity>(row));
            }
            return list;
        }

        public void Reset()
        {
            if (_parameters != null)
            {
                _parameters.Clear();
            }
            _commandText = null;
            _commandType = System.Data.CommandType.StoredProcedure; 
        }
    }
}
